/*
 * @(#)TextSearch.java
 *
 * $Date: 2009-11-17 20:56:29 -0600 (Tue, 17 Nov 2009) $
 *
 * Copyright (c) 2009 by Jeremy Wood.
 * All rights reserved.
 *
 * The copyright of this software is owned by Jeremy Wood. 
 * You may not use, copy or modify this software, except in  
 * accordance with the license agreement you entered into with  
 * Jeremy Wood. For details see accompanying license terms.
 * 
 * This software is probably, but not necessarily, discussed here:
 * http://javagraphics.blogspot.com/
 * 
 * And the latest version should be available here:
 * https://javagraphics.dev.java.net/
 */
package ig.formato.gui.app.herramientas;

import java.awt.Toolkit;
import java.lang.reflect.Constructor;

import javax.swing.UIManager;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.Segment;

//import com.bric.plaf.AbstractSearchHighlight;

/** Some static methods relating to searching a <code>JTextComponent</code>.
 * 
 */
public class TextSearch {

	/** This performs a basic search, starting from the current selection.
	 * Also this consults <code>UIManager.get("beepWhenSearchFails")</code>
	 * to determine whether to beep or not if a match is not found.
	 * 
	 * @param textComponent the component to search.
	 * @param searchPhrase the phrase to search for.
	 * @param forward whether to move forward or backward (aka "next" or "previous").
	 * @param matchCase whether to match the case or not.
	 * @return true if a match is found, false otherwise.
	 */
	public static boolean find(JTextComponent textComponent, String searchPhrase,
			boolean forward,boolean matchCase) {
		boolean returnValue = find2(textComponent,searchPhrase,forward,matchCase);
		if(!returnValue) {
			Boolean beep = (Boolean)UIManager.get("beepWhenSearchFails");
			if(beep==null) beep = Boolean.TRUE;
			if(beep.booleanValue())
				Toolkit.getDefaultToolkit().beep();
		}
		return returnValue;
	}
	
	/** This was wrapped in a separated method so all the ways
	 * to return false would still easily trigger a beep in the method above.
	 */
	private static boolean find2(JTextComponent textComponent, String searchPhrase,
			boolean forward,boolean matchCase) {
		if (textComponent == null)
			throw new NullPointerException(
					"No text component was provided to search.");
		if (searchPhrase == null)
			throw new NullPointerException(
					"No search phrase was provided to search for.");
		
		if(searchPhrase.length()==0)
			return false;

		if(matchCase==false)
			searchPhrase = searchPhrase.toUpperCase();
		
		int[] selection = new int[2];
		int startingIndex;
		if(forward) {
			startingIndex = Math.max(textComponent.getSelectionStart(), textComponent.getSelectionEnd());
		} else {
			startingIndex = Math.min(textComponent.getSelectionStart(), textComponent.getSelectionEnd());
		}
		int endIndex = forward ? textComponent.getDocument().getLength() : 0;
		if(find(textComponent, searchPhrase, forward, matchCase, true, startingIndex, endIndex, selection)) {
			int prevStart = Math.min(textComponent.getSelectionStart(), textComponent.getSelectionEnd());
			int prevEnd = Math.max(textComponent.getSelectionStart(), textComponent.getSelectionEnd());
			
			//if the selection doesn't change, do nothing.
			if(prevStart==selection[0] && prevEnd==selection[1])
				return false;
			
			textComponent.setSelectionStart(selection[0]);
			textComponent.setSelectionEnd(selection[1]);
			highlight(textComponent, selection[0], selection[1]);
			return true;
		}
		return false;
	}
	
	/** Returns the number of occurances of a phrase in a text component.
	 * 
	 * @param textComponent the text component to search.
	 * @param searchPhrase the phrase we're looking for.
	 * @param matchCase whether we have to be case sensitive or not.
	 * @return the number of occurrences of the phrase in the text component.
	 */
	public static int countOccurrence(JTextComponent textComponent,String searchPhrase,boolean matchCase) {
		if (textComponent == null)
			throw new NullPointerException(
					"No text component was provided to search.");
		if (searchPhrase == null)
			throw new NullPointerException(
					"No search phrase was provided to search for.");
		
		if(searchPhrase.length()==0)
			return 0;

		if(matchCase==false)
			searchPhrase = searchPhrase.toUpperCase();
		
		int sum = 0;
		int index = 0;
		int docLength = textComponent.getDocument().getLength();
		int[] selection = new int[2];
		while(index<docLength) {
			if(find(textComponent, searchPhrase, true, matchCase, false, index, docLength, selection)) {
				//alternatively, this could be: index = selection[1]?
				index = selection[0]+1;
				sum++;
			} else {
				return sum;
			}
		}
		return sum;
	}

	/** This performs a basic search, starting from a specific index.
	 * If a match is found, the indices are stored in the <code>selection</code> array,
	 * and <code>true</code> is returned.
	 * 
	 * @param textComponent the component to search.
	 * @param searchPhrase the phrase to search for.
	 * @param forward whether to move forward or backward
	 * @param matchCase whether to match the case or not.
	 * @param startingIndex the index in the document to begin searching.
	 * @param the array to store the indices in if a match is found.
	 * @return true if the array provided now contains two indices enclosing
	 * the search phrase.
	 */
	public static boolean find(JTextComponent textComponent, String searchPhrase,
			boolean forward,boolean matchCase,int startingIndex,int[] selection) {

		if(matchCase==false)
			searchPhrase = searchPhrase.toUpperCase();
		
		if(forward) {
			int length = textComponent.getDocument().getLength();
			return find(textComponent, searchPhrase, forward, matchCase, false, startingIndex, length, selection);
		} else {
			return find(textComponent, searchPhrase, forward, matchCase, false, startingIndex, 0, selection);
		}
	}
	
	/**
	 * 
	 * @param textComponent
	 * @param searchPhrase if matchCase is false, this must be uppercase
	 * @param forward
	 * @param matchCase
	 * @param wrapAround
	 * @param startingIndex the index at which we start reading
	 * @param endIndex the index at which we stop reading
	 * @param selection the selection start and end will be stored in this 2-int array
	 * @return true if the "selection" argument is now populated with the location of
	 * this search phrase
	 */
	private static boolean find(JTextComponent textComponent, String searchPhrase,
			boolean forward,boolean matchCase,boolean wrapAround,int startingIndex, int endIndex,int[] selection) {
		/** I don't have a lot of experience with large text documents in Java.
		 * 
		 * It would be easiest to just convert the textComponent's document into
		 * a String, and then use indexOf and lastIndexOf to perform these searches:
		 * but it seems like that might be really wasteful in terms of large
		 * documents.  (Especially consider the countOccurrence method, which may
		 * call this method thousands of times with every keystroke.)
		 * 
		 * So -- while I admit I don't know what I'm doing -- it seems like using
		 * the Segment class might be the better way to do this?
		 */
		int spl = searchPhrase.length();
		Document doc = textComponent.getDocument();
		char[] array = new char[spl];
		Segment segment = new Segment(array, 0, 0);
		
		try {
			if(forward) { //searching forward
				int index = startingIndex;
				int matching = 0;
				segment.setPartialReturn(true);
				
				while(index<endIndex) {
					int remainingCharsInDoc = endIndex-index;
					int remainingCharsInSearchPhrase = spl-matching;
					int searchLength = remainingCharsInDoc>remainingCharsInSearchPhrase ? 
							remainingCharsInSearchPhrase : remainingCharsInDoc;
					doc.getText(index, searchLength, segment);
					
					char[] workingArray;
					int arrayOffset;
					if(matchCase==false) {
						for(int a = 0; a<segment.count; a++) {
							array[a] = Character.toUpperCase( segment.array[a+segment.offset] );
						}
						workingArray = array;
						arrayOffset = 0;
					} else {
						workingArray = segment.array;
						arrayOffset = segment.offset;
					}
					
					scan : {
						for(int a = 0; a<segment.count; a++) {
							if(searchPhrase.charAt(matching)==workingArray[arrayOffset+a]) {
								matching++;
								if(matching==spl) {
									selection[1] = index+matching-(spl-segment.count);
									selection[0] = selection[1]-spl;
									return true;
								}
							} else {
								if(matching>0) {
									matching = 0;
									index = index-matching+1;
									break scan;
								}
								matching = 0;
							}
						}
						
						index+=segment.count;
					}
				}
				
				if(wrapAround && startingIndex!=0) {
					startingIndex = Math.min( textComponent.getDocument().getLength(), startingIndex+spl);
					return find(textComponent, searchPhrase, forward, matchCase, false, 0, startingIndex, selection);
				}
				return false;
			} else { //searching backward
				int index = Math.max(startingIndex-spl,0);
				int matching = 0;
				segment.setPartialReturn(false);
				
				while(index>endIndex) {
					int remainingCharsInDoc = index-endIndex;
					int remainingCharsInSearchPhrase = spl-matching;
					int searchLength = remainingCharsInDoc>remainingCharsInSearchPhrase ? 
							remainingCharsInSearchPhrase : remainingCharsInDoc;
					doc.getText(index+matching, searchLength, segment);
					
					char[] workingArray;
					int arrayOffset;
					if(matchCase==false) {
						for(int a = 0; a<segment.count; a++) {
							array[a] = Character.toUpperCase( segment.array[a+segment.offset] );
						}
						workingArray = array;
						arrayOffset = 0;
					} else {
						workingArray = segment.array;
						arrayOffset = segment.offset;
					}
					
					scan : {
						for(int a = segment.count-1; a>=0; a--) {
							if(searchPhrase.charAt(spl-1-matching)==workingArray[arrayOffset+a]) {
								matching++;
								if(matching==spl) {
									selection[0] = index+matching-(segment.count-1-a)-1;
									selection[1] = selection[0]+spl;
									return true;
								}
							} else {
								if(matching>0) {
									matching = 0;
									index = index+matching-1;
									break scan;
								}
								matching = 0;
							}
						}
						
						index-=segment.count;
					}
				}
				
				int docLength = textComponent.getDocument().getLength();
				if(wrapAround && startingIndex!=docLength) {
					startingIndex = Math.max(0, startingIndex-spl);
					return find(textComponent, searchPhrase, forward, matchCase, false, docLength, startingIndex, selection);
				}
				return false;
			}
		} catch(BadLocationException e) {
			//this shouldn't happen, right?
			RuntimeException e2 = new RuntimeException();
			e2.initCause(e);
			throw e2;
		}
	}

	/** This can create a highlight effect for a selection of text.
	 * Originally this was designed to work with the {@link com.bric.plaf.AbstractSearchHighlight} class,
	 * but it can work with any object that has a constructor accepting the
	 * same arguments this method uses.
	 * <P>By default this will use the <code>AquaSearchHighlight</code>,
	 * but you can call <Code>UIManager.put("textSearchHighlightEffect", myClassName)</code>
	 * to change this default class.
	 * 
	 * @param tc the text component.
	 * @param selectionStart the beginning of the selection to highlight.
	 * @param selectionEnd the end of the selection to highlight.
	 */
	public static void highlight(JTextComponent tc, int selectionStart,
			int selectionEnd) {
		String className = UIManager.getString("textSearchHighlightEffect");
		if(className==null) {
			className = "ig.formato.gui.app.herramientas.AquaSearchHighlight";
		}
		try {
			Class c = Class.forName(className);
			Constructor constructor = c.getConstructor(new Class[] {JTextComponent.class, Integer.TYPE, Integer.TYPE });
			constructor.newInstance(new Object[] {tc, new Integer(selectionStart), new Integer(selectionEnd)});
		} catch(Throwable t) {
			if(t instanceof RuntimeException)
				throw (RuntimeException)t;
		}
	}
	
	/*public static void clearHighlights(JTextComponent jtc) {
		AbstractSearchHighlight.clearHighlights();
	}*/
}
